﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Xml.Serialization;

namespace T4UtilityBelt.Serializers
{
    public class XsdSerializer 
    {
        #region Xsd

        private readonly XsdDictionary _xsdItems  = new XsdDictionary();
        private int PrefixCounter;

        private XNamespace _xs;
        
        // http://www.liquid-technologies.com/Tutorials/XmlSchemas/XsdTutorial_03.aspx

        public IEnumerable<Xsd> CreateXsd(IEnumerable<Type> types)
        {
            _xsdItems.Clear();
            PrefixCounter = 0;
            foreach (Type type in types)
            {
                Xsd xsd = CreateComplexTypesXsd(type);
                _xsdItems.Add(xsd.Namespace, xsd);
            }

            foreach (Type type in types)
            {
                Xsd elementXsd = CreateElementsXsd(type);
                _xsdItems.Add(elementXsd.Name + ".Element", elementXsd);
            }
            return _xsdItems.Values;
        }

        private Xsd CreateElementsXsd(Type type)
        {
            XsdDictionary xsdNamespace = new XsdDictionary();

            string name = type.Namespace;
            Xsd xsd = new Xsd
                {
                    Type = type, 
                    Name = name, 
                    Namespace = name,
                    Prefix = "p" + PrefixCounter++
                };

            _xs = "http://www.w3.org/2001/XMLSchema";
            XElement root = new XElement(_xs + "schema");
            
            // <xs:import schemaLocation="CommonTypes.xsd" namespace="http://NamespaceTest.com/CommonTypes"/> 
            root.Add(new XAttribute(XNamespace.Xmlns + "xs", "http://www.w3.org/2001/XMLSchema"));
            root.Add(new XAttribute("targetNamespace", xsd.Name));

            Assembly ass = type.Assembly;

            foreach (TypeInfo type1 in ass.DefinedTypes.Where(t => t.Namespace == name))
            {
                // <xs:element name="Name" type="xs:string" /> 
                if (typeof (IList).IsAssignableFrom(type1)
                    || typeof (IDictionary).IsAssignableFrom(type1))
                {}
                else 
                    if (!type1.IsInterface
                         && !type1.IsAbstract
                         && !type1.IsGenericType
                         && !type1.Name.Contains("<"))
                {
                    if (type1.Namespace != null
                        && _xsdItems.ContainsKey(type1.Namespace))
                    {
                        Xsd xsdNs = _xsdItems[type1.Namespace];
                        XElement xel = new XElement(_xs + "element");
                        xel.Add(new XAttribute("name", MakeElementName(type1.Name)));
                        xel.Add(new XAttribute("type", xsdNs.Prefix + ":" + MakeElementName(type1.Name)));
                        root.Add(xel);

                        if (!xsdNamespace.ContainsKey(type1.Namespace))
                        {
                            xsdNamespace.Add(type1.Namespace, xsdNs);
                        }
                    }
                    else
                    {
                        XElement xel = new XElement(_xs + "element");
                        xel.Add(new XAttribute("name", MakeElementName(type1.Name)));
                        xel.Add(new XAttribute("type", MakeElementName(type1.Name)));
                        root.Add(xel);
                    }
                }
            }

            AddImportXsd(xsdNamespace, root);


            xsd.Document = new XDocument(root);
            return xsd;
        }

        private Xsd CreateComplexTypesXsd(Type type)
        {
            XsdDictionary xsdNamespace = new XsdDictionary();

            string name = type.Namespace;
            Xsd xsd = new Xsd {Type = type, Name = name + ".ComplexTypes", Namespace = name,
                    Prefix = "p" + PrefixCounter++};

            _xs = "http://www.w3.org/2001/XMLSchema";
            XElement root = new XElement(_xs + "schema");
            root.Add(new XAttribute(XNamespace.Xmlns + "xs", "http://www.w3.org/2001/XMLSchema"));

            root.Add(new XAttribute("targetNamespace", name));
            root.Add(new XAttribute("xmlns", name));

            Assembly ass = type.Assembly;
            string ns = type.Namespace;
            foreach (TypeInfo type1 in ass.DefinedTypes.Where(t => t.Namespace == ns))
            {
                XElement xel = CreateComplexTypeXsd(type1, xsdNamespace);
                if (xel != null)
                {
                    root.Add(xel);
                }
            }

            AddImportXsd(xsdNamespace, root);

            xsd.Document = new XDocument(root);
            return xsd;
        }

        private void AddImportXsd(XsdDictionary xsdNamespace, XElement root)
        {
            foreach (Xsd x in xsdNamespace.Values)
            {
                root.Add(new XAttribute(XNamespace.Xmlns + x.Prefix, x.Namespace));

                XElement import = new XElement(_xs + "import");
                import.Add(new XAttribute("schemaLocation", x.Name + ".xsd"));
                import.Add(new XAttribute("namespace", x.Namespace));
                root.AddFirst(import);
            }
        }


        public string CreateXsd(IEnumerable<Type> types, XsdDictionary xsdNamespace)
        {
            _xs = "http://www.w3.org/2001/XMLSchema";
            XElement root = new XElement(_xs + "schema",
                new XAttribute(XNamespace.Xmlns + "xs", "http://www.w3.org/2001/XMLSchema"));

            bool first = true;
            foreach (Type type in types)
            {
                if (first)
                {
                    first = false;
                    root.Add(new XAttribute("targetNamespace", type.Namespace));
                    root.Add(new XAttribute("xmlns", type.Namespace));
                }

                Assembly ass = type.Assembly;
                string ns = type.Namespace;
                foreach (TypeInfo type1 in ass.DefinedTypes.Where(t=> t.Namespace == ns))
                {
                    XElement xel = CreateComplexTypeXsd(type1, xsdNamespace);
                    if (xel != null)
                    {
                        root.Add(xel);
                    }
                }
            }

            XDocument doc = new XDocument(root);
            return doc.ToString();
        }

        //private string GetLastName(string name)
        //{
        //    if (string.IsNullOrEmpty(name))
        //    {
        //        return null;
        //    }
        //    int i = name.IndexOf('.');
        //    if (i <= 0)
        //    {
        //        return null;
        //    }
        //    return name.Substring(i + 1);
        //}

        private static string MakeElementName(string name)
        {
            return name.Replace('`', '_');
        }

        private XElement CreateComplexTypeXsd(Type t, XsdDictionary xsdNamespace)
        {
            if (t.IsInterface
                || t.Name.Contains("<"))
            {
                return null;
            }

            try
            {
                if (t.IsEnum)
                {
                    return CreateXsdEnum(t);
                }


                //XElement xel = new XElement(xs + "element");

                XElement xct = new XElement(_xs + "complexType");
                xct.Add(new XAttribute("name", MakeElementName(t.Name)));
                XElement xseq = new XElement(_xs + "sequence");
                XElement baseElement;
                if (typeof(IList).IsAssignableFrom(t)
                    || typeof(IDictionary).IsAssignableFrom(t))
                {
                    // <xs:choice minOccurs="0" maxOccurs="unbounded">
                    xct.Add(CreateXsdXElements(t));
                    return xct;
                } 
                if (t.BaseType == null
                    || t.BaseType == typeof (object))
                {
                    xct.Add(xseq);
                    baseElement = xct;
                }
                else
                {
                    XElement xcct = new XElement(_xs + "complexContent");
                    xct.Add(xcct);
                    XElement xext = new XElement(_xs + "extension");
                    xcct.Add(xext);

                    Type type1 = t.BaseType;
                    if (type1.Namespace != null
                        && _xsdItems.ContainsKey(type1.Namespace))
                    {
                        Xsd xsdNs = _xsdItems[type1.Namespace];
                        xext.Add(new XAttribute("base", xsdNs.Prefix + ":" + MakeElementName(t.BaseType.Name)));
                        if (!xsdNamespace.ContainsKey(type1.Namespace))
                        {
                            xsdNamespace.Add(type1.Namespace, xsdNs);
                        }
                    }
                    else
                    {
                        xext.Add(new XAttribute("base", MakeElementName(t.BaseType.Name)));
                    }
                    xext.Add(xseq);
                    baseElement = xext;
                }
                //xel.Add(xct);

                AddXsdProperties(t, xseq, baseElement, xsdNamespace);

                return xct;
            }
            catch (Exception x)
            {
                throw new SerializerException("Error XElement with: " + t.FullName + " " + x.Message);
            }
        }

        private XElement AddXsdXAttribute(PropertyInfo propertyInfo, XsdDictionary xsdNamespace)
        {
            try
            {
                XElement xab = new XElement(_xs + "attribute");
                xab.Add(new XAttribute("name", propertyInfo.Name));
                xab.Add(new XAttribute("type", CreateXsdType(propertyInfo.PropertyType, xsdNamespace)));

                return xab;
            }
            catch (Exception x)
            {
                throw new SerializerException("Error XAttribute with: " + propertyInfo.Name + " " + x.Message);
            }
        }

        private XElement AddXsdXElement(PropertyInfo propertyInfo, XsdDictionary xsdNamespace)
        {
            try
            {
                XElement xab = new XElement(_xs + "element");
                xab.Add(new XAttribute("name", propertyInfo.Name));
                xab.Add(new XAttribute("type", CreateXsdType(propertyInfo.PropertyType, xsdNamespace)));

                if (XsdIsRequiredType(propertyInfo.PropertyType))
                {
                    xab.Add(new XAttribute("use", "required"));
                }

                return xab;
            }
            catch (Exception x)
            {
                throw new SerializerException("Error XAttribute with: " + propertyInfo.Name + " " + x.Message);
            }
        }

        private static bool XsdIsRequiredType(Type type)
        {
            if (typeof(int) == type
            || typeof(DateTime) == type
            || typeof(double) == type
            || typeof(decimal) == type
            || typeof(bool) == type)
            {
                return true;
            }
            return false;
        }

        private string CreateXsdType(Type type, XsdDictionary xsdNamespace)
        {
            if (typeof (int) == type
                || typeof(int?) == type)
            {
                return "xs:integer";
            }
            if (typeof(object) == type)
            {
                return "xs:string";
            }
            if (typeof(string) == type)
            {
                return "xs:string";
            }
            if (typeof (DateTime) == type
                || typeof (DateTime?) == type)
            {
                return "xs:date";
            }
            if (typeof (double) == type
                || typeof (double?) == type)
            {
                return "xs:double";
            }
            if (typeof(decimal) == type
                || typeof(decimal?) == type)
            {
                return "xs:decimal";
            }
            if (typeof(bool) == type
                || typeof(bool?) == type)
            {
                return "xs:boolean";
            }

            if (type.Namespace != null
                && _xsdItems.ContainsKey(type.Namespace))
            {
                Xsd xsdNs = _xsdItems[type.Namespace];
                if (!xsdNamespace.ContainsKey(type.Namespace))
                {
                    xsdNamespace.Add(type.Namespace, xsdNs);
                }
                return xsdNs.Prefix + ":" + MakeElementName(type.Name);
            }
            return MakeElementName(type.Name);
        }

        private void AddXsdProperties(Type t, XElement xel, XElement xcomplexType, XsdDictionary xsdNamespace)
        {
            foreach (PropertyInfo propertyInfo in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly ))
            {
                if (!HasXIgnore(propertyInfo)
                    && IsValidPropertyGet(propertyInfo))
                {
                    if (typeof(IList).IsAssignableFrom(propertyInfo.PropertyType)
                        || typeof(IDictionary).IsAssignableFrom(propertyInfo.PropertyType))
                    {
                        XElement xchild = AddXsdXElement(propertyInfo, xsdNamespace);
                        if (xchild != null)
                        {
                            xel.Add(xchild);
                        }
                        //CreateXsdXElements(propertyInfo, xel);
                    }
                    else if (!IsValidPropertySet(propertyInfo))
                    {
                    }
                    else if (typeof(string).IsAssignableFrom(propertyInfo.PropertyType) 
                             || propertyInfo.PropertyType.IsValueType)
                    {
                        XElement xchild = AddXsdXAttribute(propertyInfo, xsdNamespace);
                        if (xchild != null)
                        {
                            xcomplexType.Add(xchild);
                        }
                    }
                    else
                    {
                        XElement xchild = AddXsdXElement(propertyInfo, xsdNamespace);
                        if (xchild != null)
                        {
                            xel.Add(xchild);
                        }
                    }
                }
            }
        }

        private XElement CreateXsdEnum(Type type)
        {
            XElement xab = new XElement(_xs + "simpleType");
            xab.Add(new XAttribute("name", type.Name));

            XElement xRes = new XElement(_xs + "restriction");
            xRes.Add(new XAttribute("base", "xs:string"));
            xab.Add(xRes);

            foreach (string name in type.GetEnumNames())
            {
                XElement xEnum = new XElement(_xs + "enumeration");
                xEnum.Add(new XAttribute("value", name));
                xRes.Add(xEnum);
            }

            return xab;
        }

        private XElement CreateXsdXElements(Type type)
        {
            XElement choice = new XElement(_xs + "choice");
            choice.Add(new XAttribute("minOccurs", "0"));
            choice.Add(new XAttribute("maxOccurs", "unbounded"));

            foreach (Type t in type.GetInterfaces())
            {
                if (t.IsGenericType)
                {
                    if (t.Name == "IList`1")
                    {
                        foreach (var typeInfo in GetAssignableTypes(t.GenericTypeArguments[0]))
                        {
                            XElement listElement = new XElement(_xs + "element");
                            choice.Add(listElement);
                            listElement.Add(new XAttribute("name", typeInfo.Name));
                            listElement.Add(new XAttribute("type", typeInfo.Name));
                        }
                    }
                }
            }

            return choice;
        }

        private IEnumerable<TypeInfo> GetAssignableTypes(Type type)
        {
            foreach (TypeInfo childType  in type.Assembly.DefinedTypes)
            {
                if (!childType.IsAbstract
                    && type.Namespace == childType.Namespace
                    && type.IsAssignableFrom(childType))
                {
                    yield return childType;
                }
            }
        }

        private static bool HasXIgnore(PropertyInfo propertyInfo)
        {
            return propertyInfo.GetCustomAttribute<XIgnoreAttribute>() != null;
        }

        private static bool IsValidPropertyGet(PropertyInfo propertyInfo)
        {
            return (propertyInfo.GetMethod != null
                    && propertyInfo.GetMethod.IsPublic);
        }

        private static bool IsValidPropertySet(PropertyInfo propertyInfo)
        {
            return (propertyInfo.SetMethod != null
                    && propertyInfo.SetMethod.IsPublic);
        }

        #endregion
    }
}
